/**
 * Copyright 2006 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ocean.main.fields;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.document.Fieldable;
import org.apache.ocean.main.IndexSchema;
import org.apache.ocean.main.util.DateMathParser;

import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;

/**
 * FieldType that can represent any Date/Time with millisecond precisison.
 * <p>
 * Date Format for the XML, incoming and outgoing:
 * </p>
 * <blockquote> A date field shall be of the form 1995-12-31T23:59:59Z The
 * trailing "Z" designates UTC time and is mandatory. Optional fractional
 * seconds are allowed: 1995-12-31T23:59:59.999Z All other parts are mandatory.
 * </blockquote>
 * <p>
 * This format was derived to be standards compliant (ISO 8601) and is a more
 * restricted form of the canonical representation of dateTime from XML schema
 * part 2. http://www.w3.org/TR/xmlschema-2/#dateTime
 * </p>
 * <blockquote> "In 1970 the Coordinated Universal Time system was devised by an
 * international advisory group of technical experts within the International
 * Telecommunication Union (ITU). The ITU felt it was best to designate a single
 * abbreviation for use in all languages in order to minimize confusion. Since
 * unanimous agreement could not be achieved on using either the English word
 * order, CUT, or the French word order, TUC, the acronym UTC was chosen as a
 * compromise." </blockquote>
 */
// TODO: implement shortened dates in constantscorerangequery
public class DateFieldType extends FieldType {
	private static ThreadLocalDateFormat dateFormatThreadLocal = new ThreadLocalDateFormat();
	public static enum PatternStyle {MILLIS, SECONDS, DAY, MONTH, YEAR};
	public final static DatePattern[] parseDatePatterns;
	static {
		parseDatePatterns = new DatePattern[5];
		parseDatePatterns[0] = new DatePattern(PatternStyle.MILLIS, "yyyy-MM-dd'T'HH:mm:ss.SSS");
		parseDatePatterns[1] = new DatePattern(PatternStyle.SECONDS, "yyyy-MM-dd'T'HH:mm:ss");
		parseDatePatterns[2] = new DatePattern(PatternStyle.DAY, "yyyy-MM-dd");
		parseDatePatterns[3] = new DatePattern(PatternStyle.MONTH, "yyyy-MM");
		parseDatePatterns[4] = new DatePattern(PatternStyle.YEAR, "yyyy");
	}
	public static TimeZone UTC = TimeZone.getTimeZone("UTC");
	protected static String NOW = "NOW";
	protected static char Z = 'Z';

	public DateFieldType() {
		typeName = "date";
	}
  
	public static class DatePattern {
		private final PatternStyle patternStyle;
		private final String pattern;
		
		public DatePattern(PatternStyle patternStyle, String pattern) {
			this.patternStyle = patternStyle;
			this.pattern = pattern;
		}

		public PatternStyle getPatternStyle() {
			return patternStyle;
		}

		public String getPattern() {
			return pattern;
		}
	}
	
	// The XML (external) date format will sort correctly, except if
	// fractions of seconds are present (because '.' is lower than 'Z').
	// The easiest fix is to simply remove the 'Z' for the internal
	// format.

	protected void init(IndexSchema schema, Map<String, String> args) {
	}

	public Date parseMath(Date now, String val) throws Exception {
		String math = null;
		/* :TODO: let Locale/TimeZone come from init args for rounding only */
		final DateMathParser dateMathParser = new DateMathParser(UTC, ULocale.US);
		if (null != now)
			dateMathParser.setNow(now);
		if (val.startsWith(NOW)) {
			math = val.substring(NOW.length());
		} else {
			final int zz = val.indexOf(Z);
			if (0 < zz) {
				math = val.substring(zz + 1);
				dateMathParser.setNow((Date) stringToObject(val.substring(0, zz)));
			} else {
				throw new Exception("Invalid Date String:'" + val + '\'');
			}
		}
		if (null == math || math.equals("")) {
			return dateMathParser.getNow();
		}
		return dateMathParser.parseMath(math);
	}

	public Class getType() {
		return Date.class;
	}

	public Object stringToObject(String string) throws ParseException {
		if (StringUtils.isBlank(string))
			return null;
		return parseDate(string);
	}

	public byte[] createPayloadBuffer() {
		return new byte[8];
	}

	public byte[] objectToBytes(Object object) {
		Date date = (Date) object;
		return NumberUtils.encode(date.getTime());
	}

	public Object bytesToObject(byte[] bytes) {
		long time = NumberUtils.decode(bytes);
		return new Date(time);
	}
  
	public static class ParsedDate {
		private final PatternStyle patternStyle;
		private final Date date;
		
		public ParsedDate(PatternStyle patternStyle, Date date) {
			this.patternStyle = patternStyle;
			this.date = date;
		}

		public PatternStyle getPatternStyle() {
			return patternStyle;
		}

		public Date getDate() {
			return date;
		}
	}
	
	public Date[] parseRange(String lower, String upper) throws ParseException {
		ParsedDate lowerParsedDate = parseDate(lower);
		ParsedDate upperParsedDate = parseDate(lower);
		Date upperDate = toUpperDate(upperParsedDate);
		return new Date[] {lowerParsedDate.getDate(), upperDate};
	}
	
	public static Date toUpperDate(ParsedDate parsedDate) {
		Calendar calendar = Calendar.getInstance(UTC, ULocale.US);
		calendar.setTime(parsedDate.getDate());
		PatternStyle patternStyle = parsedDate.getPatternStyle();
		if (patternStyle.equals(PatternStyle.MILLIS)) {
		  return calendar.getTime();
		} else if (patternStyle.equals(PatternStyle.SECONDS)) {
			calendar.set(Calendar.MILLISECOND, 999);
		} else if (patternStyle.equals(PatternStyle.MONTH)) {
			int dayOfMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
			calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
			calendar.set(Calendar.HOUR_OF_DAY, 23);
			calendar.set(Calendar.MINUTE, 59);
			calendar.set(Calendar.SECOND, 59);
			calendar.set(Calendar.MILLISECOND, 999);
		} else if (patternStyle.equals(PatternStyle.YEAR)) {
			calendar.set(Calendar.MONTH, Calendar.DECEMBER);
			int dayOfMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
			calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
			calendar.set(Calendar.HOUR_OF_DAY, 23);
			calendar.set(Calendar.MINUTE, 59);
			calendar.set(Calendar.SECOND, 59);
			calendar.set(Calendar.MILLISECOND, 999);
		} 
		return calendar.getTime();
	}
	
	public static ParsedDate parseDate(String string) throws ParseException {
		int len = string.length();
		String substring = string;
		if (string.charAt(len - 1) == Z) {
			substring = string.substring(0, len - 1);
		}
		return getThreadLocalDateFormatter().parse(substring);
	}

	public static String formatDate(Date date) {
		return getThreadLocalDateFormatter().format(date) + Z;
	}

	public String objectToExternal(Object object) {
		return formatDate((Date) object);
	}

	public String objectToIndexed(Object object) {
		Date value = (Date) object;
		String string = getThreadLocalDateFormatter().format(value);
		return toInternal(string.toString());
	}

	public String toInternal(String val) {
		int len = val.length();
		if (val.charAt(len - 1) == Z) {
			return val.substring(0, len - 1);
		}
		throw new RuntimeException("Invalid Date String:'" + val + "\'");
	}

	public String indexedToReadable(String indexedForm) {
		return indexedForm + Z;
	}

	public String toExternal(Fieldable f) {
		return indexedToReadable(f.stringValue());
	}

	protected static DateFormatter getThreadLocalDateFormatter() {
		return dateFormatThreadLocal.get();
	}
  
	public static class DatePatternFormat {
		private PatternStyle patternStyle;
		private DateFormat dateFormat;
		
		public DatePatternFormat(PatternStyle patternStyle, DateFormat dateFormat) {
			this.patternStyle = patternStyle;
			this.dateFormat = dateFormat;
		}

		public PatternStyle getPatternStyle() {
			return patternStyle;
		}

		public DateFormat getDateFormat() {
			return dateFormat;
		}
	}
	
	public static class DateFormatter {
		private DateFormat formatter;
		private List<DatePatternFormat> parsers;

		public DateFormatter() {
			formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
			formatter.setTimeZone(UTC);
			parsers = new ArrayList<DatePatternFormat>(parseDatePatterns.length);
			for (DatePattern datePattern : parseDatePatterns) {
				SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern.getPattern(), Locale.US);
				parsers.add(new DatePatternFormat(datePattern.getPatternStyle(), dateFormat));
			}
		}

		public String format(Date date) {
			return formatter.format(date);
		}

		public ParsedDate parse(String string) throws ParseException {
			Iterator<DatePatternFormat> iterator = parsers.iterator();
			while (iterator.hasNext()) {
				DatePatternFormat datePatternFormat = iterator.next();
				try {
					Date date = datePatternFormat.getDateFormat().parse(string);
					return new ParsedDate(datePatternFormat.getPatternStyle(), date);
				} catch (ParseException parseException) {
          if (!iterator.hasNext()) {
          	throw parseException;
          }
				}
			}
			return null;
		}
	}

	private static class ThreadLocalDateFormat extends ThreadLocal<DateFormatter> {
		public ThreadLocalDateFormat() {
			super();
		}

		protected DateFormatter initialValue() {
			return new DateFormatter();
		}
	}
}
